Python: Align HandoffBuilder participant type signatures with runtime Agent-only requirement#4302
Conversation
…entRun (microsoft#4301) HandoffBuilder.participants() accepted SupportsAgentRun by API contract, but build() failed at runtime because _prepare_agent_with_handoffs() requires Agent instances for cloning, tool injection, and middleware. Fix: Update all public type hints, docstrings, and validation in HandoffBuilder and HandoffAgentExecutor to require Agent explicitly. The isinstance check is now performed early in participants() with a clear error message explaining why Agent is required. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
moonbox3
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 3 | Confidence: 85%
✓ Correctness
This is a clean type-narrowing refactor that replaces the broader
SupportsAgentRunprotocol with the concreteAgentclass throughout the handoff module. The runtime isinstance check inparticipants()was correctly updated, and a new test validates the stricter behavior. The only notable issue is an inconsistent docstring inwith_autonomous_modethat now mentions bothSupportsAgentRunandAgentdespite the type having been narrowed toAgentonly. No correctness bugs were found.
✓ Security Reliability
This diff narrows the accepted type from the SupportsAgentRun protocol to the concrete Agent class throughout the handoff system. The public entry point (HandoffBuilder.participants) retains a runtime isinstance check, but the runtime guard inside _prepare_agent_with_handoffs was removed. Since HandoffAgentExecutor is a public class whose init now accepts agent: Agent without any runtime validation, a caller bypassing HandoffBuilder could pass a non-Agent object and hit undefined behavior in _clone_chat_agent. This is a defense-in-depth gap, not a critical vulnerability. Otherwise the change is straightforward and the new test correctly validates the public-API guard.
✓ Test Coverage
The diff narrows type constraints from SupportsAgentRun to Agent across the handoff module and adds one test validating that participants() rejects non-Agent SupportsAgentRun instances. The test is well-structured with a meaningful assertion (TypeError match). However, coverage gaps exist: the runtime guard removed from _prepare_agent_with_handoffs has no replacement test, and other public API methods that changed signatures (add_handoff, with_start_agent) lack rejection tests. The constructor's participants= kwarg path is implicitly covered since it delegates to participants(), but that's not explicitly verified.
Suggestions
- In
with_autonomous_mode, the docstring on line 947 was changed to saySupportsAgentRun / Agent instancesbut the type annotation was narrowed toSequence[Agent]. This should just sayAgent instancesto be consistent with the rest of the refactor. - The runtime
isinstance(agent, Agent)check was removed from_prepare_agent_with_handoffs(the old lines 253-254). While the builder'sparticipants()validates inputs,HandoffAgentExecutorcan still be constructed directly without the builder, so callers bypassing the builder lose the runtime safety net. Consider keeping a runtime guard in__init__or_prepare_agent_with_handoffsfor defense-in-depth. - Consider keeping a lightweight runtime isinstance check (or assert) at the top of HandoffAgentExecutor.init or _prepare_agent_with_handoffs. The old TypeError guard was removed (around old line 253), but HandoffAgentExecutor is a public class — callers that construct it directly bypass HandoffBuilder.participants() validation, and Python type hints are not enforced at runtime.
- Add a test that passes a non-Agent SupportsAgentRun to add_handoff(source=...) and add_handoff(targets=[...]) to verify those entry points also reject non-Agent instances consistently.
- Add a test that passes a non-Agent to with_start_agent() to verify the narrowed type constraint at that entry point.
- Consider adding a test for HandoffBuilder(participants=[fake]) (constructor kwarg path) to explicitly verify it delegates to the validated participants() method, even though it's implicitly covered.
- The runtime TypeError guard was removed from _prepare_agent_with_handoffs (old lines 253-254). If a non-Agent somehow bypasses the participants() check (e.g., via internal misuse), the error will now surface as an opaque failure in _clone_chat_agent. Consider adding a regression test or restoring a guard with a clear error message in _prepare_agent_with_handoffs.
Automated review by moonbox3's agents
python/packages/orchestrations/agent_framework_orchestrations/_handoff.py
Show resolved
Hide resolved
python/packages/orchestrations/agent_framework_orchestrations/_handoff.py
Show resolved
Hide resolved
python/packages/orchestrations/agent_framework_orchestrations/_handoff.py
Show resolved
Hide resolved
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Pull request overview
This PR fixes a type mismatch in HandoffBuilder where the API accepted SupportsAgentRun in type signatures but enforced Agent-only at runtime, causing late failures with confusing error messages. The fix narrows all public type annotations from SupportsAgentRun to Agent, moves validation into the participants() method with a clear error message, removes the redundant runtime check in _prepare_agent_with_handoffs, and adds a test confirming early rejection of non-Agent SupportsAgentRun implementors.
Changes:
- Updated type signatures to require
Agentinstead ofSupportsAgentRunthroughoutHandoffBuilderandHandoffAgentExecutor - Moved
isinstance(participant, Agent)validation from_prepare_agent_with_handoffstoparticipants()with improved error messaging - Added test to verify non-
AgentSupportsAgentRunimplementations are rejected early with a descriptive error
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
python/packages/orchestrations/agent_framework_orchestrations/_handoff.py |
Updated type signatures from SupportsAgentRun to Agent in HandoffBuilder and HandoffAgentExecutor; moved validation to participants() method; updated documentation to clarify Agent-only requirement |
python/packages/orchestrations/tests/test_handoff.py |
Added test test_handoff_builder_rejects_non_agent_supports_agent_run() to verify early rejection of non-Agent SupportsAgentRun implementations |
python/packages/orchestrations/agent_framework_orchestrations/_handoff.py
Show resolved
Hide resolved
|
my only question is what happens with things like ClaudeAgent and CopilotAgent with this? are those not possible for handoffs? |
Motivation and Context
HandoffBuilderacceptedSupportsAgentRunin its type signatures (e.g.participants(),add_handoff(),with_start_agent()), but_prepare_agent_with_handoffsraised aTypeErrorat runtime if the participant was not anAgentinstance. This mismatch caused confusing late failures and incorrect API documentation.Fixes #4301
Description
The root cause was that the public API surface of
HandoffBuilderusedSupportsAgentRunas the participant type, while the internal implementation relied onAgent-specific capabilities (cloning, tool injection, middleware) and enforced this with a runtimeisinstancecheck buried in_prepare_agent_with_handoffs. The fix narrows all public type annotations fromSupportsAgentRuntoAgent, moves the validation intoparticipants()with a clear error message, removes the redundant runtime check in_prepare_agent_with_handoffs, and adds a test confirming that non-AgentSupportsAgentRunimplementors are rejected early with a descriptiveTypeError.Contribution Checklist